在编写驱动的时候我们常常会使用
module_init或者含有该宏的函数或者宏。但是该部分如何工作的我们今天需要探讨清楚。 这里使用的是RK3588-kernel-6.1.75
这里有几个重要的宏会影响module_init展开的结果,我们现在来先确定一下:
shell
CONFIG_LTO_CLANG=n #是 Linux 内核中用于支持“链接时间优化”(Link Time Optimization,LTO)的配置选项。具体来说,它是一个内核配置宏,用于指示编译内核时是否启用 Clang 编译器的链接时间优化功能。
CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=y #是一个用于 Linux 内核的配置选项,指示特定体系结构(arch)是否支持使用 32 位相对重定位(prel32 relocations)。这通常与二进制代码的地址计算和内存布局有关。宏展开实例分析
字数
1452 字
阅读时间
7 分钟
主线分析
接下来我们就开始慢慢来展开一个宏,并且解读其中的内容,这里以drivers/input/evdev.c中的:
c
module_init(evdev_init);接着我们来开始展开第一层:
c
#define module_init(x) __initcall(x);
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall(fn, 6)
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
//当前展开结果为:
___define_initcall( evdev_init, 6, .initcall6)接着展开___define_initcall:
c
#define ___define_initcall(fn, id, __sec) \
__unique_initcall(fn, id, __sec, __initcall_id(fn))
// 当前展开结果为:
__unique_initcall( evdev_init, 6, .initcall6, __initcall_id( evdev_init))
// 这里,参考后面的章节可得
__initcall_id( evdev_init) = kmod_evdev__412_1441_evdev_init
// 当前展开结果为:
__unique_initcall( evdev_init, 6, .initcall6, kmod_evdev__412_1441_evdev_init)__unique_initcall的定义为:
c
#define __unique_initcall(fn, id, __sec, __iid) \
____define_initcall(fn, \
__initcall_stub(fn, __iid, id), \
__initcall_name(initcall, __iid, id), \
__initcall_section(__sec, __iid))
// 当前展开结果为:
____define_initcall( evdev_init, __initcall_stub( evdev_init, kmod_evdev__412_1441_evdev_init, 6), __initcall_name( initcall, kmod_evdev__412_1441_evdev_init, 6), __initcall_section( .initcall6, kmod_evdev__412_1441_evdev_init))上面的__unique_initcall有4个参数,其中__initcall_stub(fn, __iid, id) 可展开为:
c
#define __initcall_stub(fn, __iid, id) fn
// __initcall_stub展开结果为:
evdev_init而__initcall_name展开为:
c
/* Format: __<prefix>__<iid><id> */
#define __initcall_name(prefix, __iid, id) \
__PASTE(__, \
__PASTE(prefix, \
__PASTE(__, \
__PASTE(__iid, id))))
// __initcall_name展开结果为:
__initcall__kmod_evdev__412_1441_evdev_init6接着就是__initcall_section,它被展开为:
c
#define __initcall_section(__sec, __iid) \
#__sec ".init"
// __initcall_section展开结果为:
".initcall6.init"合并到主宏上去就是:
c
// 当前展开结果为:
____define_initcall( evdev_init, evdev_init, __initcall__kmod_evdev__412_1441_evdev_init6, ".initcall6.init")我们继续看下去:
c
#define ____define_initcall(fn, __stub, __name, __sec) \
__define_initcall_stub(__stub, fn) \
asm(".section \"" __sec "\", \"a\" \n" \
__stringify(__name) ": \n" \
".long " __stringify(__stub) " - . \n" \
".previous \n");
// 当前展开结果为:
__define_initcall_stub(evdev_init, evdev_init) \
asm(".section \".initcall6.init\", \"a\" \n" \
__stringify(__initcall__kmod_evdev__412_1441_evdev_init6) ": \n" \
".long " __stringify(evdev_init) " - . \n" \
".previous \n");这里需要分两步分来解:
__define_initcall_stubasm先处理__define_initcall_stub:
c
#define __define_initcall_stub(__stub, fn) \
__ADDRESSABLE(fn)
#define __ADDRESSABLE(sym) \
static void * __section(".discard.addressable") __used \
__UNIQUE_ID(__PASTE(__addressable_,sym)) = (void *)&sym;
// __define_initcall_stub展开结果为:
static void * __attribute__((__section__(section)))(".discard.addressable") __used
__UNIQUE_ID(__PASTE(__addressable_, evdev_init)) = (void *)&evdev_init;
// 这里的__UNIQUE_ID(有不同的定义,我们去其中一个代替)
#define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__)
// __define_initcall_stub展开结果为:
static void * __attribute__((__section__(section)))(".discard.addressable") __used
__UNIQUE_ID___addressable_evdev_init_412 = (void *)&evdev_init;接着处理内嵌汇编代码部分,汇编代码为:
assembly
.section ".initcall6.init", "a"
__initcall__kmod_evdev__412_1441_evdev_init6:
.long evdev_init - .
.previous这里我们参考GUN汇编代码手册,可以大致分析:
.section ".initcall6.init", "a":为.initcall6.init段追加内容。.long:指令用于在段中插入一个长整型数(通常为4字节)。evdev_init - .:这里evdev_init是一个初始化函数的名称,- .表示计算evdev_init到当前地址(.)之间的偏移量。这个偏移量是用来在运行时找到目标函数的。这种方式允许内核在初始化过程中动态查找和调用对应的函数。.previous:指令结束当前段的定义,返回到之前的段。这在汇编代码中用于切换回先前的段,确保后续代码不会影响当前定义的段。 所以就是在.initcall6.init定义了一个label(__initcall__kmod_evdev__412_1441_evdev_init6),该label储存着初始化函数evdev_init的偏移量。
那么最终的展开结果是:
c
static void * __attribute__((__section__( ".discard.addressable"))) __used
__UNIQUE_ID___addressable_evdev_init_412 = (void *)&evdev_init;
asm://这里汇编就不以内嵌形式展示
.section ".initcall6.init", "a"
__initcall__kmod_evdev__412_1441_evdev_init6:
.long evdev_init - .
.previous这里__attribute__((__section__))是GCC的功能,表示该声明放入指定section(段)中。 __used防止未显示的调用而导致的报错。
__initcall_id
这里我们需要单独展开__initcall_id,该宏函数主要提供唯一的ID号:
c
/* Format: <modname>__<counter>_<line>_<fn> */
#define __initcall_id(fn) \
__PASTE(__KBUILD_MODNAME, \
__PASTE(__, \
__PASTE(__COUNTER__, \
__PASTE(_, \
__PASTE(__LINE__, \
__PASTE(_, fn))))))
#define ___PASTE(a,b) a##b
#define __PASTE(a,b) ___PASTE(a,b)这里的__PASTE是一个拼接函数,主要是将a和b拼接起来。这里有几个重要的定义与内建变量:
__KBUILD_MODNAME__:它是一个宏定义,是由makfile控制的,在编译evdev.c过程中会自动判断该模块的名称并设置,我们可以在evdev.c目录下找到隐藏文件.evdev.o.cmd,里面有该宏定义。
__COUNTER__:它是一个GCC内建的变量,但是GCC手册并没有说明,我们参考网络上的解释大致可以理解该变量使用一次就自增一次。 那么该宏在这里的展开为:
c
kmod_evdev__412_1441_evdev_init当热,__COUNTER__是无法预测展开的,我们从
上面这些文件获取(实际上上图是__initcall_name的,而__initcall_id是其中部分)。
总结
module_init主要是:
- 声明一个为整个工程唯一的函数名
__UNIQUE_ID___addressable_evdev_init_412,它是指向evdev_init初始化函数。 - 将该函数指针放入到段
.discard.addressable中 - 将
evdev_init的偏移位置写入到段(section)中的__initcall__kmod_evdev__412_1441_evdev_init6符号中。
